<?PHP
/*
 * $Id: barge-web-multiple.classes.php 139 2009-12-16 15:29:56Z namba0219 $
 * =============================================================================
 * Barge - Web application framework on PHP5
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * =============================================================================
 * For more information, see http://barge.googlecode.com/
 */
namespace barge\web\multiple;

use \Exception;
use \ReflectionClass;

use barge\ClassLoader;
use barge\System;
use barge\logging\LogFactory;
use barge\io\File;
use barge\io\FileNotFoundException;
use barge\utils\ArrayList;
use barge\utils\Bean;
use barge\utils\BeanUtils;
use barge\utils\Config;
use barge\utils\ExceptionUtils;
use barge\utils\HashMap;
use barge\utils\IllegalAccessException;
use barge\utils\IllegalArgumentException;
use barge\utils\String;
use barge\utils\Selector;
use barge\utils\TimeValue;
use barge\utils\UndefinedConfigException;
use barge\utils\LocalString;
use barge\utils\URLUtils;
use barge\utils\XMLConfig;

use barge\web\ModuleManager;


/**
 * @version 1.0
 * @since Barge 1.2
 * @see Exception
 */
class ModuleConfigException extends Exception {};

/**
 * @see ModuleManger
 */
class MultipleModuleManager implements ModuleManager
{
	/**
	 * @var string
	 */
	private $moduleDir;

	/**
	 * @var HashMap
	 */
	private $modules;

	/**
	 * @param string $moduledir
	 */
	public function __construct($moduledir)
	{
		$this->moduleDir = $moduledir;
		$this->modules = new HashMap();

		if (!is_dir($this->moduleDir)) {
			$message = "モジュールディレクトリ {$this->moduleDir} が存在しません。";
			throw new Exception($message);
		}
	}

	/**
	 * Load module directories.
	 */
	public function load()
	{
		$log = LogFactory::getLogger();
		$tree = new ModuleTree();


		$log->debug('Load and analizing module tree ...');
		$log->info('ModuleManager : Load modules.');


		$dir = dir($this->moduleDir);
		while (($file = $dir->read()) !== false) {
			$inipath = $this->moduleDir.'/'.$file.'/module.xml';
			if (in_array($file[0], array('#', '.')) || !file_exists($inipath))
				continue;

			// Load manifest files
			$id = null;
			$ini = XMLConfig::load($inipath);
			/* @var $moduleini Config */
			$moduleini = $ini->getProperty('Module');
			if (($id = $moduleini->getProperty('name')) === null) {
				$message = "プラグインマニフェスト '{$inipath}' の解析エラー : name は必須です。";
				throw new ModuleConfigException($message);
			}

			$restore = array();
			if (System::isNotEmpty($moduleini->getProperty('requires'))) {
				foreach ($ini->getProperty('Module/requires') as $reqid => $value) {
					$restore[$reqid] = $value->getProperty('version', self::DEFAULT_VERSION);
				}
			}

			// TODO :
			$moduleini->setProperty('_requires', $restore);
			$moduleini->setProperty('_moduleDir', '/'.$file);
			//$ini->setProperty('_enabled', !file_exists($this->moduleDir.'/'.$file.'/.disabled'));

			$descriptor = new ModuleDescriptor(
				$moduleini->getProperty('name'),
				$moduleini->getProperty('version'),
				$moduleini->getProperty('_moduleDir'),
				($moduleini->getBoolProperty('required')),
				true);
//			$descriptor->setName($moduleini->getProperty('name'));
//			$descriptor->setDescription($$moduleini->getProperty('description'));
//			$descriptor->setProvider($moduleini->getProperty('provider'));
//			$descriptor->setUrl($moduleini->getProperty('url'));
			$mesage = null;
			foreach ($restore as $module => $version)
				$mesage .= $module.'(バージョン:'.$version.')　';
			$descriptor->setErrorMessage('依存モジュール '.$mesage.'が解決できません。');
			$descriptor->setMarkDisabled(true);
			$this->registerModule($descriptor);

			$log->info('ModuleManager : Loading '.$id.' ...');

			// 既に登録されているが新しいバージョンがあった場合
			$data =& $tree->getNodeData($id);
			if ($tree->existsNode($id) && $data !== null) {
				if (strcmp($data->getProperty('version'), $moduleini->getProperty('version')) < 0) {	// 今回の方が新しい
					$tree->setNodeData($id, $moduleini);
				}
				continue;
			}

			if (count($moduleini->getProperty('_requires')) > 0) {
				foreach ($moduleini->getProperty('_requires') as $reqid => $version) {
					if ($tree->existsNode($id) && $tree->existsNode($reqid)) {
					// 既に誰かが依存するモジュールとして登録していた
						$tree->setNodeData($id, $moduleini);
						// 自分が依存するもジュールの子にコピー
						$tree->createNodeLink($id, $reqid);
					} else if ($tree->existsNode($reqid)) {
					// 依存するモジュールは登録されているが自分はいない
						$tree->createChildNode($id, $moduleini, $reqid);
					} else {
					// First
						// 依存するモジュールを親として作成
						$tree->createTopNode($reqid, null);
						// 親の子として自分を登録する
						$tree->createChildNode($id, $moduleini, $reqid);
					}
				}
			} else {
				if ($tree->existsNode($id)) {
				// Already registered as required module.
				// 既に依存されるモジュールとして登録されている
					$tree->setNodeData($id, $moduleini);

				} else {
				// 誰にも依存されない
					// Append top node.
					// TOPレベルにノードを追加する
					$tree->createTopNode($id, $moduleini);
				}
			}
		}

		if ($tree->count() === 0) {
			$message = "ロード可能な有効なモジュールがありません。";
			throw new ModuleConfigException($message);
		}

		// Initialize modules.
		$tree->load($this);

		// 初期化処理
		foreach ($tree->getModuleLoaders() as $loader) {
			$loader->init($this);
		}

	}

	/**
	 * @param string $moduleId Module ID
	 * @return bool
	 * @see ModuleManger#existsModule
	 */
	public function existsModule($moduleId)
	{
		return $this->modules->contains($moduleId);
	}

	/**
	 * @param string $moduleId
	 */
	public function getModuleConfig($moduleId)
	{
//		$this->modules->put($descriptor->getId(), $descriptor);
	}

	/**
	 * @param string $moduleId
	 */
	public function getModuleDescriptor($moduleId)
	{
		return $this->modules->get($moduleId);
	}

	/**
	 * @param string $id
	 * @param Module $module
	 */
	public function registerModule(ModuleDescriptor $descriptor)
	{
		$this->modules->put($descriptor->getId(), $descriptor);
	}
}





/**
 * @version 1.0
 * @since Barge 1.2
 */
class ModuleTree
{
	protected $nodeTree = array();

	/**
	 * IDをインデックスとしたデータ配列
	 * @var array
	 */
	public $idIndexedData = array();

	/**
	 * @var array
	 */
	protected $idToNodeNoIndex = array();

	/**
	 * @var array
	 */
	protected $nodeNoToIdIndex = array();

	/**
	 * @var ArrayList
	 */
	protected $loaders;

	/**
	 * @var int
	 */
	protected $nodeNo = 1;


	/**
	 * コンストラクタ
	 */
	public function __construct()
	{
		$this->loaders = new ArrayList();
	}

	/**
	 * @param string $id
	 * @param mixed $nodeData
	 */
	public function createTopNode($id, $nodeData = null)
	{
		// 新しいノードを作成
		$nodeNo = 'node_'.$this->nodeNo;
		$this->nodeTree[$nodeNo] = Array('parent'=>false, 'childs'=>Array());
		// インデックスを作成
		$this->idToNodeNoIndex[$id] = $nodeNo;
		$this->nodeNoToIdIndex[$nodeNo] = $id;
		// データを格納
		$this->idIndexedData[$id] =& $nodeData;

		$this->nodeNo++;
	}

	/**
	 * @param string $id
	 * @param mixed $nodeData
	 * @param string $parentId
	 */
	public function createChildNode($id, $nodeData, $parentId)
	{
		// 親のノード番号を取得
		$parentNodeNo = $this->idToNodeNoIndex[$parentId];
		// 新しい子ノードを作成する
		$nodeNo = 'node_'.$this->nodeNo;
		$this->nodeTree[$nodeNo] = array('parent'=>true, 'childs'=>array());
		// インデックスを作成
		$this->idToNodeNoIndex[$id] = $nodeNo;
		$this->nodeNoToIdIndex[$nodeNo] = $id;
		// データを格納
		$this->idIndexedData[$id] =& $nodeData;
		// 親の子として自分を登録する
		$this->nodeTree[$parentNodeNo]['childs'][] = $nodeNo;

		$this->nodeNo++;
	}

	/**
	 * @param string $id
	 * @param string $parentId
	 */
	public function createNodeLink($id, $parentId)
	{
		// 自分のノード番号を取得
		$nodeNo = $this->idToNodeNoIndex[$id];
		// 親のノード番号を取得
		$parentNodeNo = $this->idToNodeNoIndex[$parentId];
		// TOPレベル要素かどうか判定
		if ($this->nodeTree[$nodeNo]['parent'] === false) {
			//自分の親を設定
			$this->nodeTree[$nodeNo]['parent'] = true;
			// 新しい親に子として追加
			$this->nodeTree[$parentNodeNo]['childs'][] = $nodeNo;
		} else {
			// リンクを作成
			$this->nodeTree[$parentNodeNo]['childs'][] = $nodeNo;
		}
	}

	/**
	 * @return int
	 */
	public function count()
	{
		return count($this->nodeTree);
	}

	/**
	 * @param string $id
	 * @return array
	 */
	public function &getNodeData($id)
	{
		return $this->idIndexedData[$id];
	}

	/**
	 * 指定したモジュールIDに対して属性を設定します
	 * @param string $id
	 * @param array $data 属性データ
	 */
	public function setNodeData($id, $data)
	{
		$this->idIndexedData[$id] = $data;
	}

	/**
	 * 指定されたモジュールIDに対するノードが存在するかどうか
	 * @param string $id
	 * @return bool
	 */
	public function existsNode($id)
	{
		return isset($this->idToNodeNoIndex[$id]);
	}

	public function dump()
	{
		// TOPレベル要素を収集
		$topNodes = array();
		foreach ($this->nodeTree as $key => $value)
			if ($value['parent'] === false)
				$topNodes[] = $key;
		if (count($topNodes) === 0) {
			$message = "TOPレベルノードが存在しないためロードできません。";
			throw new ModuleConfigException($message);
		}
		$this->_dump($topNodes);
	}

	/**
	 * @param array $tree
	 * @param int $level
	 */
	private function _dump(array $tree, $level = 0)
	{
		$padding = str_repeat('　', $level);
		if ($level > 10) {
			echo $padding.'more...';
			return;
		}
		foreach ($tree as $nodeNo) {
			// ツリーの属性を取得
			$node = $this->nodeTree[$nodeNo];
			// ノード番号からIDを逆引き
			$id = $this->nodeNoToIdIndex[$nodeNo];
			if (isset($this->idIndexedData[$id])) {
				echo $padding."<b>{$id}</b><br>";
			} else {
				echo $padding."{$id}<br>";
			}
			$this->_dump($node['childs'], $level + 1);
		}

	}

	/**
	 * @param ModuleManager $manager
	 */
	public function load(ModuleManager $manager)
	{
		$log = LogFactory::getLogger();
		$log->debug('Init modules ...');

		// TOPレベル要素を収集
//		$topNodes = array_($this->nodeTree, array($this, 'filterTop'));
		$topNodes = Array();
		foreach ($this->nodeTree as $key => &$value)
			if ($value['parent'] === false)
				$topNodes[] = $key;
		if (count($topNodes) === 0) {
			$message = "親ノードが存在しないためモジュールをロードできません。";
			throw new ModuleConfigException($message);
		}

		$this->_load($manager, $topNodes);
	}

	/**
	 * @param ModuleManager $manager
	 * @param array $tree
	 * @param int $level
	 * @param string $parentId
	 */
	private function _load(ModuleManager $manager, array $tree, $level = 0, $parentId = null)
	{
		$log = LogFactory::getLogger();

		foreach ($tree as $nodeNo) {
			// ツリーの属性を取得
			$node =& $this->nodeTree[$nodeNo];

			// ノード番号からIDを逆引き
			$id = $this->nodeNoToIdIndex[$nodeNo];
			$log->debug('ModuleManager : Init module '.$id.' ...');
			if (isset($this->idIndexedData[$id])) {
				// データ取得
				/* @var $data Config */
				$data =& $this->idIndexedData[$id];

				// 親がいる場合はバージョンを比較
				if ($parentId !== null) {
					// 親のID・バージョン取得
					$parentVersion = $this->idIndexedData[$parentId]->getProperty('version');
					// バージョンの解決
					if (count($data->getProperty('_requires')) > 0) {
						// TODO : バージョン比較
						if (strcmp($data->getProperty('_requires', $parentId), $parentVersion) > 0) {
							$log->debug("Unmatch required version "
								."{$id}({$data->getProperty('_requires', $parentId)}) > {$parentId}({$parentVersion})");
							$manager->getModuleDescriptor($id)->setErrorMessage(
									"依存モジュール {$id}(バージョン:{$data->getProperty('_requires', $parentId)}) は ...");
							continue;
						} else {
							// 解決できたものにマーク（消す）
							$data->removeProperty('_requires', $parentId);
						}
					}
				}
				// すべて解決できたら依存するモジュールをロード
				if (count($data->getProperty('_requires')) == 0) {
					if ($data->getProperty('_enabled')) {
						// モジュールのロード処理
						$classLoader = System::getClassLoader();
						if (($value = $data->getProperty('classpath')) !== null) {
							$classpaths = split(',', $value);
							foreach ($classpaths as $classpath) {
								$classLoader->addIncludePath(
									$manager->getRealPath($data->getProperty('_moduleDir').'/'.$classpath));
							}
						}
						if (($value = $data->getProperty('includes')) !== null) {
							$includeList = split(',', $value);
							foreach ($includeList as $include) {
								if (!empty($include)) {
									$classLoader->includeModule(
										$manager->getRealPath($data->getProperty('_moduleDir').'/'.$include));
								}
							}
						}
						// Execute loader, if loader specificated.
						if (($value = $data->getProperty('loaderClass')) !== null) {
							$success = true;
							try {
								$loaderInstance = $classLoader->newInstance($value);
								if ($loaderInstance->load($manager) === false) {
									$log->warn('ModuleManager : Fail to #load : '.$id);
									continue;
								}
							} catch (Exception $e) {
								$log->warn('ModuleManager : Fail to #init : '.$id);
								$manager->getModuleDescriptor($id)->setErrorMessage(
									'ModuleLoader が例外をスローしました。理由:'.$e->getMessage());
								continue;
							}
						}
//						// モジュールパラメータを処理する
//						if (($config = $data->getProperty('ModuleContext')) !== null) {
//							if (($value = $config->getProperty('templatePath')) !== null)
//								$manager->registerTemplatePath($id, $value);
//							if (($value = $config->getProperty('defaultActionId')) !== null)
//								$manager->registerDefaultActionId($id, $value);
//							if (($value = $config->getProperty('actionPattern')) !== null)
//								$manager->registerActionPattern($id, $value);
//							if (($value = $config->getProperty('formPattern')) !== null)
//								$manager->registerFormPattern($id, $value);
//							if (($value = $config->getProperty('actionFilters')) !== null)
//								foreach ($value as $filterClass)
//									$manager->registerActionFilter(new $filterClass());
//						}
//						// メッセージを登録する
//						if (($value = $data->getProperty('MessageResources')) !== null) {
//							$messageResources = MessageResources::getInstance();
//							$messageResources->addMessages(new HashMap($value));
//						}
//						if (($value = $data->getProperty('BeanContainer')) !== null) {
//							$beanManager = BeanManager::getInstance();
//							foreach ($value as $beanId => $config) {
//								$arguments = ($config->getProperty('constructArgs') != null ? new HashMap($config->getProperty('constructArgs')->toArray()) : new HashMap());
//								$properties = ($config->getProperty('properties') != null ? new HashMap($config->getProperty('properties')->toArray()) : new HashMap());
//								$beanManager->registerBeanConfig(
//									$beanId, $config->getProperty('class'),
//									$config->getProperty('singleton'),
//									$arguments,
//									$properties);
//							}
//						}
					}

					// ロード済みをマーク
					$manager->getModuleDescriptor($id)->setMarkDisabled(false);
					$manager->getModuleDescriptor($id)->setErrorMessage(null);
					if ($log->isDebugEnabled()) {
						$log->debug('ModuleManager : Load successfull module : '.$id);
					}

					// 履歴に追加
					$this->loadedModules[] = array( 'id' => $id,  );

					$this->_load($manager, $node['childs'], $level + 1, $id);

					// リリース処理
					unset($data);
				}
			} else {
				$log->debug("ModuleManager : There are no required module : {$id}");
			}
		}
	}

	/**
	 * @return ArrayList
	 */
	public function getModuleLoaders()
	{
		return $this->loaders;
	}
}

/**
 * @version 1.0
 * @since Barge 1.2
 * @see Bean
 */
class ModuleDescriptor extends Bean
{

	/**
	 * @var string
	 */
	protected $id;
	protected $description;
	protected $version;

	/**
	 * モジュールが必須モジュールかどうか　
	 * @var bool
	 */
	protected $required = false;
	protected $name;
	protected $modulepath;
//	protected $provider;
//	protected $url;

	/**
	 * ユーザーによる許可が有効かどうか 　
	 * @var bool
	 */
	protected $enabled;

	/**
	 * なんらかの原因でモジュールが無効とマークされているかどうか
	 * @var bool
	 */
	protected $markDisabled;

	protected $disabledMessage;

	/**
	 * @param string $id
	 * @param string $version
	 * @param string $moduleDir
	 */
	public function __construct($id, $version, $moduleDir, $required = false, $enabled = true)
	{
		$this->id = $id;
		$this->version = $version;
		$this->required = $required;
		$this->modulepath = $moduleDir;
		$this->markDisabled = false;
		$this->enabled = $enabled;
	}

	public function getVersion()
	{
		return $this->version;
	}

	public function getModulePath()
	{
		return $this->modulepath;
	}

	public function getId()
	{
		return $this->id;
	}

//	public function getProvider()
//	{
//		return $this->provider;
//	}

//	public function getUrl()
//	{
//		return $this->url;
//	}
//
//	public function setUrl($url)
//	{
//		$this->url = $url;
//	}
//
//	public function setProvider($provider)
//	{
//		$this->provider = $provider;
//	}
//
	public function getMarkDisabled()
	{
		return $this->markDisabled;
	}

	public function getDescription()
	{
		return $this->description;
	}

	public function setDescription($description)
	{
		$this->description = $description;
	}

	public function setName($name)
	{
		$this->name = $name;
	}

	public function getName()
	{
		return $this->name;
	}

	public function setMarkDisabled($value)
	{
		$this->markDisabled = $value;
	}

	public function setErrorMessage($message)
	{
		$this->disabledMessage = $message;
	}

	public function getRequired()
	{
		return $this->required;
	}

	public function getEnabled()
	{
		return $this->enabled;
	}

	public function getErrorMessage()
	{
		return $this->disabledMessage;
	}
}
